/*   This file is part of Super Luigi Bros.
 *
 *   Super Luigi Bros is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   Super Luigi Bros is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with Super Luigi Bros.  If not, see <http://www.gnu.org/licenses/>.
 */
package luigibros.data.xml.level;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

import luigibros.util.LazyList;
/** A map object to hold the sprite information
 *  for the classes with their various states
 *  and actions such that this information can
 *  be stored in an XML file.  
 * @author RWisneiwski1101
 */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "image_map")
public class ImageMap {
	/** A container used to hold the path and
	 *  frame number of an images in this zip
	 *  file.  
	 * @author R.Wisniewski1101
	 */
	@XmlAccessorType(XmlAccessType.FIELD)
	@XmlType
	public static final class SpriteRecord {
		/** The file path, in relation to the root
		 *  of the zip file, to the image file.  
		 */
		@XmlElement(required = true, nillable = false)
		private String path;
		/** The frame number of this image in the action.  
		 */
		@XmlElement(required = true, nillable = false, name = "frame_number")
		private int frameNum;
		/** Create an empty record. <br /><br />
		 *  **THIS IS ONLY PRESENT BECAUSE IT IS REQUIRED BY JAXB.  DO NOT USE!  **
		 */
		protected SpriteRecord() {}
		/** Create a record with the given path and frame number.  
		 * @param path - the file path to the image in relation to the zip root folder.  
		 * @param frameNum - the frame number of this image in the action.  
		 */
		public SpriteRecord(String path, int frameNum) {
			this.path = path;
			this.frameNum = frameNum;
		}
		/** Get the path of the image.  
		 * @return - the file path.  
		 */
		public String path() {return path;}
		/** Get the frame number of this image in the action.  
		 * @return - the frame number.  
		 */
		public int frame() {return frameNum;}
		@Override
		public int hashCode() {
			return path.hashCode() + 31 * frameNum;
		}
		@Override
		public boolean equals(Object obj) {
			//if it is being compared to a frame number (for searching)
			if(obj instanceof Integer) {
				return frameNum == ((Integer)obj).intValue();
			//if it is being compared to a record
			} else if(obj instanceof SpriteRecord) {
				SpriteRecord record = (SpriteRecord)obj;
				return (path == null ? record.path == null : path.equals(record.path)) && frameNum == record.frameNum;
			//I don't know how to do this, so no.  
			} else return false;
		}
	}
	/** A wrapper class used to assign a name
	 *  to a list of SpriteRecord.  This is used
	 *  to emulate the function of a Map using
	 *  Lists, to be compatible with JAXB.  
	 * @author R.Wisniewski1101
	 */
	@XmlAccessorType(XmlAccessType.FIELD)
	@XmlType
	private static class ActionMap {
		/** The name of the action mapped in this class.  
		 */
		@XmlAttribute(required = true)
		public String name;
		/** The frames for this action.  
		 */
		@XmlElement(required = true, nillable = false, name = "record")
		public List<SpriteRecord> records;
		/** Create an empty record. <br /><br />
		 *  **THIS IS ONLY PRESENT BECAUSE IT IS REQUIRED BY JAXB.  DO NOT USE!  **
		 */
		protected ActionMap() {
			records = new LazyList<SpriteRecord>();
		}
		/** Create a list with the given name and the given first frame.  
		 * @param name - the name of this action
		 * @param record - the first frame.  
		 */
		public ActionMap(String name, SpriteRecord record) {
			this();
			this.name = name;
			records.add(record);
		}
		/** Add a frame to the list, removing
		 *  another frame if they share a common
		 *  frame number.  
		 * @param record - the record to add
		 * @return - the path of the record removed (null if the frame number was unique)
		 */
		public String add(SpriteRecord record) {
			String returnVal = null;
			int index = records.indexOf(record.frame());
			//if the frame number is found
			if(index != -1) {
				//get the path to return it.  
				returnVal = records.get(index).path();
				records.remove(index);
			}
			records.add(record);
			return returnVal;
		}
		@Override
		public int hashCode() {
			return records.hashCode() + name.hashCode();
		}
		@Override
		public boolean equals(Object obj) {
			//if this is being compared to an action name
			if(obj instanceof String) {
				return name == null ? obj == null : name.equals(obj);
			//if this is being compared to another action
			} else if (obj instanceof ActionMap) {
				ActionMap map = (ActionMap)obj;
				return (name == null ? map.name == null : name.equals(map.name)) && (records == null ? map.records == null : records.equals(map.records));
			//I don't know how to do this, so no.  
			} else return false;
		}
	}
	/** A wrapper class used to assign a name
	 *  to a list of Actions.  This is used
	 *  to emulate the function of a Map using
	 *  Lists, to be compatible with JAXB.  
	 * @author R.Wisniewski1101
	 */
	@XmlAccessorType(XmlAccessType.FIELD)
	@XmlType
	private static class StateMap{
		/** The name of the state.  
		 */
		@XmlAttribute(required = true)
		public String name;
		/** The actions in this state.  
		 */
		@XmlElement(required = true, nillable = false, name = "action")
		public List<ActionMap> actions;
		/** Create an empty record. <br /><br />
		 *  **THIS IS ONLY PRESENT BECAUSE IT IS REQUIRED BY JAXB.  DO NOT USE!  **
		 */
		protected StateMap() {
			actions = new LazyList<ActionMap>();
		}
		/** Create a state with a given name
		 *  and a given action.  
		 * @param state - the state name
		 * @param map - the given action
		 */
		public StateMap(String state, ActionMap map) {
			this();
			this.name = state;
			actions.add(map);
		}
		/** Get a map version of the actions
		 *  this class contains.  
		 * @return - a map version of the actions.  
		 */
		public Map<String, List<SpriteRecord>> getActionsMap() {
			Map<String, List<SpriteRecord>> map = new HashMap<String, List<SpriteRecord>>();
			for(ActionMap action : actions)
				map.put(action.name, action.records);
			return map;
		}
		@Override
		public int hashCode() {
			return name.hashCode() + actions.hashCode();
		}
		@Override
		public boolean equals(Object obj) {
			//if this is being compared to a state name
			if (obj instanceof String) {
				return name == null ? false : name.equals(obj);
			//if this is being compared to another state
			} else if(obj instanceof StateMap) {
				StateMap map = (StateMap)obj;
				return (name == null ? map.name == null : name.equals(map.name)) && (actions == null ? map.actions == null : actions.equals(map.actions));
			//I don't know how to do this, so no.  
			} else return false;
		}
	}
	/** A wrapper class used to assign a name
	 *  to a list of States.  This is used
	 *  to emulate the function of a Map using
	 *  Lists, to be compatible with JAXB.  
	 * @author R.Wisniewski1101
	 */
	@XmlAccessorType(XmlAccessType.FIELD)
	@XmlType
	private static class ClassMap {
		/** The fully qualified class name to
		 *  which to map images. <br/>
		 *  This can be acquired with
		 *  <code>Object.getClass().getName()</code>   
		 */
		@XmlAttribute(required = true)
		public String name;
		/** The states for this class.  
		 */
		@XmlElement(required = true, nillable = false, name = "state")
		public List<StateMap> states;
		/** Create an empty record. <br /><br />
		 *  **THIS IS ONLY PRESENT BECAUSE IT IS REQUIRED BY JAXB.  DO NOT USE!  **
		 */
		protected ClassMap() {
			states = new LazyList<StateMap>();
		}
		/** Create a record for the given class
		 *  the given state.  
		 * @param className - the fully qualified name of the class this record represents
		 * @param map - the first state.  
		 */
		public ClassMap(String className, StateMap map) {
			this();
			this.name = className;
			states.add(map);
		}
		/** Get a map representation of the
		 *  underlying states.  
		 * @return - the states as a map.  
		 */
		public Map<String, Map<String, List<SpriteRecord>>> getStateMap() {
			Map<String, Map<String, List<SpriteRecord>>> map = new HashMap<String, Map<String, List<SpriteRecord>>>();
			for(StateMap state : states)
				map.put(state.name, state.getActionsMap());
			return map;
		}
		@Override
		public int hashCode() {
			return name.hashCode() + states.hashCode();
		}
		@Override
		public boolean equals(Object obj) {
			//if this is being compared to a class name
			if (obj instanceof String) {
				return name == null ? false : name.equals(obj);
			//if this is being compared to another class
			} else if(obj instanceof ClassMap) {
				ClassMap map = (ClassMap)obj;
				return (name == null ? map.name == null : name.equals(map.name)) && (states == null ? map.states == null : states.equals(map.states));
			//I don't know how to do this, so no.  
			} else return false;
		}
	}
	/** The records
	 */
	@XmlElement(required = true, nillable = false, name = "class")
	private List<ClassMap> classes;
	/** Creates an empty map.  
	 */
	public ImageMap() {
		classes = new LazyList<ClassMap>();
	}
	/** Get the frames from the map.  Null if not found.  
	 * @param className - the class name
	 * @param state - the state name
	 * @param action - the action name
	 * @return - the frames for that class-state-action group.  
	 */
	public List<SpriteRecord> get(String className, String state, String action) {
		int index = classes.indexOf(className);
		//does not exist?  
		if(index ==  -1)
			return null;
		ClassMap cMap = classes.get(index);
		index = cMap.states.indexOf(state);
		//does not exist?  
		if(index == -1)
			return null;
		StateMap sMap = cMap.states.get(index);
		index = sMap.actions.indexOf(action);
		//does not exist?  
		if(index == -1)
			 return null;
		return sMap.actions.get(index).records;
	}
	/** Add a frame for the given class-state-action set.  
	 * @param className - the class name
	 * @param state - the state name
	 * @param action - the action name
	 * @param record - the frame
	 */
	public void put(String className, String state, String action, SpriteRecord record) {
		int index = classes.indexOf(className);
		//exists?  
		if(index != -1) {
			ClassMap cMap = classes.get(index);
			index = cMap.states.indexOf(state);
			//exists? 
			if(index != -1) {
				StateMap sMap = cMap.states.get(index);
				index = sMap.actions.indexOf(action);
				//exists? 
				if(index != -1)
					sMap.actions.get(index).add(record);
				else //add it
					sMap.actions.add(new ActionMap(action, record));
			} else //add it
				cMap.states.add(new StateMap(state, new ActionMap(action, record)));
		} else //add it
			classes.add(new ClassMap(className, new StateMap(state, new ActionMap(action, record))));
	}
	/** Get a map representation of the contents of this map.  
	 * @return
	 */
	public Map<String, Map<String, Map<String, List<SpriteRecord>>>> getClassMap() {
		Map<String, Map<String, Map<String, List<SpriteRecord>>>> map = new HashMap<String, Map<String, Map<String, List<SpriteRecord>>>>();
		for(ClassMap _class : classes)
			map.put(_class.name, _class.getStateMap());
		return map;
	}
	@Override
	public int hashCode() {
		return classes.hashCode();
	}
	@Override
	public boolean equals(Object obj) {
		return classes.equals(obj);
	}
	/** A simple program to generate and read the XML output from this file.  
	 * @param args
	 * @throws JAXBException
	 * @throws IOException
	 */
	public static void main (String[] args) throws JAXBException, IOException {
		Scanner in = new Scanner(System.in);
		String input;
		boolean read, create;
		do {
			System.out.print("Create or read file [C / R]:  ");
			input = in.nextLine().trim();
			read = "r".equalsIgnoreCase(input);
			create = "c".equalsIgnoreCase(input);
			if(!read && !create)
				System.out.println("Please enter a valid option.  ");
		} while(!read && !create);
		if(create) {
			ImageMap map = new ImageMap();
			boolean yes = false, no = false;
			do {
				System.out.print("Class name:  ");
				String className = in.next();
				do {
					System.out.print("State name:  ");
					String state = in.next();
					do {
						System.out.print("Action name:  ");
						String action = in.nextLine();
						do {
							System.out.print("Frame number:  ");
							int frame = in.nextInt();
							System.out.print("Image file path:  ");
							String path = in.next();
							
							map.put(className, state, action, new SpriteRecord(path, frame));
							do {
								System.out.print("Create another frame for this action [Y / n]:  ");
								input = in.next();
								yes = "y".equalsIgnoreCase(input);
								no = "n".equalsIgnoreCase(input);
								if(!yes && !no)
									System.out.println("Please enter a 'y' or an 'n' only.  ");
							} while (!yes && !no);
						} while (!no);
						do {
							System.out.print("Create another action for this state [Y / n]:  ");
							input = in.next();
							yes = "y".equalsIgnoreCase(input);
							no = "n".equalsIgnoreCase(input);
							if(!yes && !no)
								System.out.println("Please enter a 'y' or an 'n' only.  ");
						} while (!yes && !no);
					} while (!no);
					do {
						System.out.print("Create another state for this class [Y / n]:  ");
						input = in.next();
						yes = "y".equalsIgnoreCase(input);
						no = "n".equalsIgnoreCase(input);
						if(!yes && !no)
							System.out.println("Please enter a 'y' or an 'n' only.  ");
					} while (!yes && !no);
				} while (!no);
				do {
					System.out.print("Create another class [Y / n]:  ");
					input = in.next();
					yes = "y".equalsIgnoreCase(input);
					no = "n".equalsIgnoreCase(input);
					if(!yes && !no)
						System.out.println("Please enter a 'y' or an 'n' only.  ");
				} while (!yes && !no);
			} while(!no);
			System.out.println("Creating file...  ");
			File file = new File("imageData.cfg.xml");
			file.createNewFile();
			System.out.println("Creating XML context...");
			Marshaller m = JAXBContext.newInstance("luigibros.data.xml").createMarshaller();
			System.out.println("Writing data to file...");
			m.marshal(map, file);
			System.out.println("Done.  ");
		} else if(read) {
			Unmarshaller m = JAXBContext.newInstance("luigibros.data.xml").createUnmarshaller();
			ImageMap iMap = (ImageMap) m.unmarshal(new File("imageData.cfg.xml"));
			Map<String, Map<String, Map<String, List<SpriteRecord>>>> cMap = iMap.getClassMap();
			for(String c :cMap.keySet()) {
				System.out.println(c);
				Map<String, Map<String, List<SpriteRecord>>> sMap = cMap.get(c);
				for(String s : sMap.keySet()) {
					System.out.println("\t" + s);
					Map<String, List<SpriteRecord>> aMap = sMap.get(s);
					for(String a : aMap.keySet()) {
						List<SpriteRecord> records = aMap.get(a);
						System.out.println("\t\t" + a);
						for(SpriteRecord sp : records)
							System.out.println("\t\t\t" + sp.frame() + " '" + sp.path() + "'");
					}
				}
			}
		}
	}
}
